use std::{cell::RefCell, collections::HashMap, rc::Rc};

use crate::{expr::FuncProto, BuiltinMacro, Expr};

use super::{value::BuiltinLambda, Appliable, Lambda, Value};

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Unsupported Conversion")]
    UnsupportedConvert,
    #[error("Unexpected Params")]
    UnexpectedParams,
    #[error("No such variable('{0}')")]
    NoSuchVariable(String),
}

#[derive(Clone)]
pub struct Env {
    parent: Option<Rc<Env>>,
    values: RefCell<HashMap<String, Value>>,
}

impl Env {
    pub fn root() -> Rc<Self> {
        Rc::new(Self {
            parent: None,
            values: RefCell::new(HashMap::new()),
        })
    }

    pub fn new(parent: Rc<Env>) -> Rc<Self> {
        Rc::new(Self {
            parent: Some(parent),
            values: RefCell::new(HashMap::new()),
        })
    }

    pub fn set<S: Into<String>>(&self, name: S, value: Value) {
        self.values.borrow_mut().insert(name.into(), value);
    }

    pub fn set_value<S: Into<String>, I: Into<Value>>(&self, name: S, value: I) {
        self.set(name.into(), value.into())
    }

    pub fn set_macro<S: Into<String>>(&self, name: S, builtin: BuiltinMacro) {
        self.set(name.into(), Value::BuiltinMacro(builtin))
    }

    pub fn set_lambda<S: Into<String>>(&self, name: S, builtin: Rc<RefCell<dyn Appliable>>) {
        self.set(name.into(), Value::BuiltinLambda(BuiltinLambda(builtin)))
    }

    pub fn make_lambda<S: Into<String>, L: Appliable + 'static>(&self, name: S, builtin: L) {
        self.set(name.into(), Value::BuiltinLambda(BuiltinLambda(Rc::new(RefCell::new(builtin)))));
    }

    pub fn get(&self, name: &str) -> super::Result<Value> {
        if let Some(val) = self.values.borrow().get(name).map(Value::clone) {
            Ok(val)
        } else if let Some(parent) = &self.parent {
            parent.get(name)
        } else {
            Err(Error::NoSuchVariable(name.into()).into())
        }
    }

    pub fn remove(&self, name: &str) -> super::Result<Value> {
        self.values
            .borrow_mut()
            .remove(name)
            .ok_or(Error::NoSuchVariable(name.to_string()).into())
    }
}

pub trait EnvExt {
    fn deep_clone(&self) -> Rc<Env>;
    fn eval(&self, expr: Expr) -> super::Result<Value>;
    fn apply(&self, value: Value, params: Vec<Expr>) -> super::Result<Value>;
    fn eval_apply(&self, expr: Expr, params: Vec<Expr>) -> super::Result<Value>;
}

impl EnvExt for Rc<Env> {
    fn deep_clone(&self) -> Rc<Env> {
        Rc::new(self.as_ref().clone())
    }

    fn eval(&self, expr: Expr) -> super::Result<Value> {
        match expr {
            Expr::Nil => Ok(Value::Nil),
            Expr::Compound(mut exprs) => {
                if let Some(first) = exprs.pop() {
                    return self.eval(first)?.apply(self.clone(), exprs);
                }
                Ok(Value::Nil)
            }
            Expr::List(exprs) => {
                let mut values = vec![];
                for expr in exprs {
                    values.push(self.eval(expr)?);
                }
                Ok(Value::List(values))
            }
            Expr::Quoted(expr) => Ok(Value::Expr(*expr)),
            Expr::Literal(literal) => Ok(Value::from(literal)),
            Expr::Ident(id) => self.get(&id),
            Expr::Proto(FuncProto { params, body }) => Ok(Value::Lambda(Box::new(Lambda::new(
                self.clone(),
                params,
                *body,
            )))),
            Expr::Fn(_, _) => todo!(),
            Expr::Def(id, val) => {
                self.set(id, self.eval(*val)?);
                Ok(Value::Nil)
            }
            Expr::Operation(op, exprs) => op.apply(self.clone(), exprs),
            Expr::Block(mut exprs) => loop {
                if exprs.len() == 1 {
                    break self.eval(exprs.pop().unwrap());
                }
                let _ = self.eval(exprs.pop().unwrap());
            },
            Expr::Let(params, body) => {
                let local = Env::new(self.clone());

                for param in params
                    .into_compound_iter()
                    .ok_or(Error::UnexpectedParams)?
                {
                    let mut pair = param.into_compound().ok_or(Error::UnexpectedParams)?;

                    let id = pair
                        .pop()
                        .and_then(Expr::into_ident)
                        .ok_or(Error::UnexpectedParams)?;
                    let expr = pair.pop().ok_or(Error::UnexpectedParams)?;

                    local.set(id, local.eval(expr)?);
                }

                local.eval(*body)
            }
        }
    }

    fn eval_apply(&self, expr: Expr, params: Vec<Expr>) -> super::Result<Value> {
        self.apply(self.eval(expr)?, params)
    }

    fn apply(&self, value: Value, params: Vec<Expr>) -> super::Result<Value> {
        value.apply(self.clone(), params)
    }
}
